#define FUSE_USE_VERSION 26

/* For pread()/pwrite() */
#define _XOPEN_SOURCE 500

#include <iostream>

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/statvfs.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <sys/xattr.h>

#include "s_fuse_backend.h"


namespace dhd
{
namespace server
{


   struct MissingFields{};


   
   PBuffer FuseBackend::handle_request(ToServer & request, ToClient & reply, PBuffer & inbuff)
   {
       int (FuseBackend::* fp)(const std::string &, ToServer &, ToClient &, PBuffer &, PBuffer &) = 0;
       
       reply.set_msg_type( request.msg_type() );
       
       if ( !request.has_path() )
       {
           reply.set_result( -EFAULT );
           std::cout << "Missing PATH field for message of type: " << request.msg_type() << std::endl;
           return 0;
       }
       
       switch( request.msg_type() )
       {
         case dhd::GETATTR:         fp = &FuseBackend::be_getattr;      break;
         case dhd::ACCESS:          fp = &FuseBackend::be_access;       break;
         case dhd::READ_LINK:       fp = &FuseBackend::be_readlink;     break;
         case dhd::READ_DIR:        fp = &FuseBackend::be_readdir;      break;
         case dhd::MKNOD:           fp = &FuseBackend::be_mknod;        break;
         case dhd::MKDIR:           fp = &FuseBackend::be_mkdir;        break;
         case dhd::UNLINK:          fp = &FuseBackend::be_unlink;       break;
         case dhd::RMDIR:           fp = &FuseBackend::be_rmdir;        break;
         case dhd::SYMLINK:         fp = &FuseBackend::be_symlink;      break;
         case dhd::RENAME:          fp = &FuseBackend::be_rename;       break;
         case dhd::LINK:            fp = &FuseBackend::be_link;         break;
         case dhd::CHMOD:           fp = &FuseBackend::be_chmod;        break;
         case dhd::CHOWN:           fp = &FuseBackend::be_chown;        break;
         case dhd::TRUNCATE:        fp = &FuseBackend::be_truncate;     break;
         case dhd::UTIMENS:         fp = &FuseBackend::be_utimens;      break;
         case dhd::OPEN:            fp = &FuseBackend::be_open;         break;
         case dhd::READ:            fp = &FuseBackend::be_read;         break;
         case dhd::WRITE:           fp = &FuseBackend::be_write;        break;
         case dhd::STATFS:          fp = &FuseBackend::be_statfs;       break;
         case dhd::RELEASE:         fp = &FuseBackend::be_release;      break;
         case dhd::FSYNC:           fp = &FuseBackend::be_fsync;        break;
         case dhd::SETXATTR:        fp = &FuseBackend::be_setxattr;     break;
         case dhd::GETXATTR:        fp = &FuseBackend::be_getxattr;     break;
         case dhd::LISTXATTR:       fp = &FuseBackend::be_listxattr;    break;
         case dhd::REMOVEXATTR:     fp = &FuseBackend::be_removexattr;  break;
           
         case dhd::DHD_SERVER_ROOT: fp = &FuseBackend::dhd_server_root; break;
           
         default:
           std::cout << "Invalid message type: " << request.msg_type() << std::endl;
       }

       PBuffer outbuff;
       int r = -EFAULT;
       
       try
       {
           if (fp)
           {
               std::string path( root + request.path() );
               
               r = (this->*fp)( path, request, reply, inbuff, outbuff );
           }
       }
       catch (MissingFields &e)
       {
           std::cout << "Missing required fields for message of type: " << request.msg_type() << std::endl;
       }
       
       reply.set_result( r );
       
       return outbuff;
   }
   
   /************************************************************/
   
   int FuseBackend::be_getattr(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       struct stat stbuf;
       
       std::cerr << "**** GETATTR CALLED *****" << std::endl;
       
       if (!request.has_path() ) throw MissingFields();
       
       if (lstat(path.c_str(), &stbuf) == -1)
           return -errno;
       
       dhd::Stat *s = reply.mutable_fstat();
       
       s->set_st_size    ( stbuf.st_size    );
       s->set_st_dev     ( stbuf.st_dev     );
       s->set_st_ino     ( stbuf.st_ino     );
       s->set_st_nlink   ( stbuf.st_nlink   );
       s->set_st_mode    ( stbuf.st_mode    );
       s->set_st_uid     ( stbuf.st_uid     );
       s->set_st_gid     ( stbuf.st_gid     );
       s->set_st_rdev    ( stbuf.st_rdev    );
       s->set_st_blksize ( stbuf.st_blksize );
       s->set_st_blocks  ( stbuf.st_blocks  );
       
       s->mutable_st_atimespec()->set_tv_sec  ( stbuf.st_atim.tv_sec  );
       s->mutable_st_atimespec()->set_tv_nsec ( stbuf.st_atim.tv_nsec );
       s->mutable_st_mtimespec()->set_tv_sec  ( stbuf.st_mtim.tv_sec  );
       s->mutable_st_mtimespec()->set_tv_nsec ( stbuf.st_mtim.tv_nsec );
       s->mutable_st_ctimespec()->set_tv_sec  ( stbuf.st_ctim.tv_sec  );
       s->mutable_st_ctimespec()->set_tv_nsec ( stbuf.st_ctim.tv_nsec );
       
       return 0;
   }
   
   // TODO: Something intelligent here
   int FuseBackend::be_access(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() || !request.has_access_mask()) throw MissingFields();
   
       int res = access(path.c_str(), request.access_mask());
       
       return res == -1 ? -errno : 0;
   }
   

   int FuseBackend::be_readlink(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path()) throw MissingFields();
       
       int res;
       
       char buf[16 * 1024];
       
       res = readlink(path.c_str(), buf, sizeof(buf) - 1);
       
       if (res == -1)
           return -errno;
       
       buf[res] = '\0';
       
       outbuff = allocate_buffer( res + 1 );
       
       strncpy( outbuff->data(), buf, res + 1 );
       
       return 0;
   }


   int FuseBackend::be_readdir(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path()) throw MissingFields();
       
       DIR *dp;
       struct dirent *de;
       
       
       dp = opendir(path.c_str());
       
       if (dp == NULL)
           return -errno;
       
       dhd::DirEntry * rde = 0;
       
       while ((de = readdir(dp)) != NULL) 
       {
           rde = reply.add_dir_entries();
           rde->set_name(  de->d_name );
           rde->set_inode( de->d_ino );
           rde->set_mode(  de->d_type << 12 );
       }
       
       closedir(dp);
       return 0;
   }

   
   int FuseBackend::be_mknod(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() || !request.has_mode() || !request.has_rdev()) throw MissingFields();
       
       int res;
       
       mode_t mode = request.mode();
       dev_t  rdev = request.rdev();
       
       // On Linux this could just be 'mknod(path, mode, rdev)' but this is more portable
       if (S_ISREG(mode)) 
       {
           res = open(path.c_str(), O_CREAT | O_EXCL | O_WRONLY, mode);
           if (res >= 0)
               res = close(res);
       } 
       else if (S_ISFIFO(mode))
       {
           res = mkfifo(path.c_str(), mode);
       }
       else
       {
           // TODO: We probably want to deny this request outright. 
           res = mknod(path.c_str(), mode, rdev);
       }
       
       return res == -1 ? -errno : 0;
   }
   
   
   int FuseBackend::be_mkdir(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() || !request.has_mode() ) throw MissingFields();
       
       int res = mkdir(path.c_str(), request.mode());
       
       return res == -1 ? -errno : 0;
   }

   
   int FuseBackend::be_unlink(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() ) throw MissingFields();
       
       int res = unlink(path.c_str());
       
       return res == -1 ? -errno : 0;
   }
   

   int FuseBackend::be_rmdir(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() ) throw MissingFields();
       
       int res = rmdir(path.c_str());
       
       return res == -1 ? -errno : 0;
   }
   
   
   int FuseBackend::be_symlink(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() || !request.has_to()) throw MissingFields();
       
       std::cerr << "Creating symlink link(" << request.to().c_str() << ", " << path.c_str() << ")\n";
       int res = symlink(request.to().c_str(), path.c_str());
       
       return res == -1 ? -errno : 0;
   }

   
   int FuseBackend::be_rename(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() || !request.has_to()) throw MissingFields();
       
       std::string local_to = root + request.to();
       
       std::cerr << "Renaming file(" << local_to.c_str() << ", " << path.c_str() << ")\n";
       
       int res = rename(local_to.c_str(), path.c_str());
       
       return res == -1 ? -errno : 0;
   }
   
   
   int FuseBackend::be_link(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() || !request.has_to()) throw MissingFields();

       std::string local_to = root + request.to();
       
       std::cerr << "Creating hardlink link(" << local_to.c_str() << ", " << path.c_str() << ")\n";
       int res = link(local_to.c_str(), path.c_str());
       
       return res == -1 ? -errno : 0;
   }
   

   int FuseBackend::be_chmod(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() || !request.has_mode()) throw MissingFields();
       
       int res = chmod(path.c_str(), request.mode());
       
       return res == -1 ? -errno : 0;
   }

   
   int FuseBackend::be_chown(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() || !request.has_uid() || !request.has_gid()) throw MissingFields();
       
       int res = lchown(path.c_str(), request.uid(), request.gid());
       
       return res == -1 ? -errno : 0;
   }
   

   int FuseBackend::be_truncate(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() || !request.has_size()) throw MissingFields();
       
       int res = truncate(path.c_str(), request.size());
       
       return res == -1 ? -errno : 0;
   }
   
   
   int FuseBackend::be_utimens(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() || !request.times_size() == 2) throw MissingFields();
       
       struct timeval tv[2];
       
       tv[0].tv_sec  = request.times(0).tv_sec();
       tv[0].tv_usec = request.times(0).tv_nsec() / 1000;
       tv[1].tv_sec  = request.times(1).tv_sec();
       tv[1].tv_usec = request.times(1).tv_nsec() / 1000;
       
       int res = utimes(path.c_str(), tv);
       
       return res == -1 ? -errno : 0;
   }
   
   // TODO: Cache open file descriptors
   int FuseBackend::be_open(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() || !request.has_flags()) throw MissingFields();
       
       int fd = open(path.c_str(), request.flags());
       
       if (fd == -1)
           return -errno;
       
       close(fd);
       
       reply.set_open_id( fd );
       
       return 0;
   }
   
   // TODO: Use cached file descriptors
   int FuseBackend::be_read(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() || !request.has_size() || !request.has_offset() || !request.has_open_id()) throw MissingFields();
       
       int fd;
       int res;
       
       fd = open(path.c_str(), O_RDONLY);
       
       if (fd == -1)
           return -errno;
       
       outbuff = allocate_buffer( request.size() );
       
       res = pread(fd, outbuff->data(), request.size(), request.offset());
       
       close(fd);
       
       std::cout << "READ Path: " << path << " req size = " << request.size() << " req off = " << request.offset() << " actual = " << res << std::endl;
       
       // Set size to amount actually read
       if ( res >= 0 )
           outbuff->set_size( res );
       else
           outbuff = 0;
       
       return res == -1 ? -errno : res;
   }
   
   
   int FuseBackend::be_write(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() || !request.has_size() || !inbuff || inbuff->size() == 0 || !request.has_offset() || !request.has_open_id()) throw MissingFields();
       
       int fd;
       int res;
       
       fd = open(path.c_str(), O_WRONLY);
       
       if (fd == -1)
           return -errno;
       
       res = pwrite(fd, inbuff->data(), inbuff->size(), request.offset());
       
       close(fd);
       
       return res == -1 ? -errno : res;
   }

   
   int FuseBackend::be_statfs(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path()) throw MissingFields();
       
       int res;
       struct statvfs s;

       res = statvfs(path.c_str(), &s);
       
       if (res == -1)
           return -errno;
       
       ::dhd::StatVFS* v = reply.mutable_vstat();
       
       v->set_f_bsize  ( s.f_bsize  );
       v->set_f_frsize ( s.f_frsize );
       v->set_f_blocks ( s.f_blocks );
       v->set_f_bfree  ( s.f_bfree  );
       v->set_f_bavail ( s.f_bavail );
       v->set_f_files  ( s.f_files  );
       v->set_f_ffree  ( s.f_ffree  );
       v->set_f_favail ( s.f_favail );
       
       return 0;
   }

   
   int FuseBackend::be_release(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       // const char *path, struct fuse_file_info *fi
       // 
       /* Just a stub.     This method is optional and can safely be left
        unimplemented */
       
       return 0;
   }

   
   int FuseBackend::be_fsync(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       //const char *path, int isdatasync, struct fuse_file_info *fi
       
       /* Just a stub.     This method is optional and can safely be left
        unimplemented */
       
       return 0;
   }


   
   int FuseBackend::be_setxattr(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       // const char *path, const char *name, const char *value, size_t size, int flags
       // 
       if (!request.has_path() || !request.has_to() || !inbuff || inbuff->size() == 0 || !request.has_flags()) throw MissingFields();
       
       int res = lsetxattr(path.c_str(), request.to().c_str(), inbuff->data(), inbuff->size(), request.flags());
    
       return res == -1 ? -errno : 0;
   }
   
   
   int FuseBackend::be_getxattr(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() || !request.has_to() || !request.has_size() ) throw MissingFields();
       
       if ( request.size() == 0 )
           return 0;
       
       outbuff = allocate_buffer( request.size() );
       
       int res = lgetxattr(path.c_str(), request.to().c_str(), outbuff->data(), request.size());
       
       if ( res >= 0 )
           outbuff->set_size( res );
       else
           outbuff = 0;
       
       return res == -1 ? -errno : res;
   }

   
   int FuseBackend::be_listxattr(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() || !request.has_size() ) throw MissingFields();
       
       outbuff = allocate_buffer( request.size() );
       
       int res = llistxattr(path.c_str(), outbuff->data(), request.size());
       
       if ( res >= 0 )
           outbuff->set_size( res );
       else
           outbuff = 0;
       
       return res == -1 ? -errno : res;
   }

   
   int FuseBackend::be_removexattr(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       if (!request.has_path() || !request.has_to() ) throw MissingFields();
       
       int res = lremovexattr(path.c_str(), request.to().c_str());
       
       return res == -1 ? -errno : 0;
   }
    
   
   int FuseBackend::dhd_server_root(const std::string & path, ToServer & request, ToClient & reply, PBuffer & inbuff, PBuffer & outbuff)
   {
       DIR *dp;
       struct dirent *de;
       
       dp = opendir(root.c_str());
       
       if (dp == NULL)
           return -errno;
       
       dhd::DirEntry * rde = 0;
       
       while ((de = readdir(dp)) != NULL)
       {
           if ( !(strcmp( de->d_name, "." ) == 0 || strcmp(de->d_name, "..") == 0 ) )
           {
               rde = reply.add_dir_entries();
               rde->set_name(  de->d_name );
               rde->set_inode( de->d_ino );
               rde->set_mode(  de->d_type << 12 );
           }
       }
       
       closedir(dp);
       
       struct statvfs s;
       
       int res = statvfs(root.c_str(), &s);
       
       if (res == -1)
           return -errno;
       
       ::dhd::StatVFS* v = reply.mutable_vstat();
       
       v->set_f_bsize  ( s.f_bsize  );
       v->set_f_frsize ( s.f_frsize );
       v->set_f_blocks ( s.f_blocks );
       v->set_f_bfree  ( s.f_bfree  );
       v->set_f_bavail ( s.f_bavail );
       v->set_f_files  ( s.f_files  );
       v->set_f_ffree  ( s.f_ffree  );
       v->set_f_favail ( s.f_favail );
       
       return 0;
   }
   
}//end namespace dhd::server
}//end namespace dhd

